package com.gitee.fsmxm.zd.bili;

import com.gitee.fsmxm.zd.bili.entity.DanmuHost;
import com.gitee.fsmxm.zd.bili.entity.EnterRoomInfo;
import com.gitee.fsmxm.zd.bili.listener.MyWebSocketAdapter;
import com.google.gson.*;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.compress.compressors.brotli.BrotliCompressorInputStream;
import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream;
import org.java_websocket.handshake.ServerHandshake;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

public class Main {

    private static final OkHttpClient client = new OkHttpClient.Builder()
            .pingInterval(30L, TimeUnit.SECONDS)
            .build();

    private static final Gson gson = new Gson();

    private static DanmuWebSocketClient danmuWebsocketClient;

    private static DanmuHost[] danmuHosts;
    private static int roomId = -1;
    private static int hostIndex;
    private static String key = "";

    private static final Object monitor = new Object();

    public static void main(String[] args) {
        int liveId = 213;
        System.out.println("正在解析房间号...");
        getRoomId(liveId);
        if (roomId == -1) {
            return;
        }
        System.out.println("房间号: " + roomId);

        System.out.println("正在获取弹幕服务器...");
        getDanmuHosts();
        if (danmuHosts == null || danmuHosts.length == 0) {
            return;
        }
        System.out.println("获取弹幕服务器成功");

        String url = getWsUrl();
        System.out.println("正在连接弹幕服务器...");
        danmuWebsocketClient = new DanmuWebSocketClient(URI.create(url));
        danmuWebsocketClient.setListener(new DanmuWebSocketAdapter());
        danmuWebsocketClient.connect();

    }

    private static String getWsUrl() {
        return "wss://" + danmuHosts[hostIndex].getHost() + ":" + danmuHosts[hostIndex].getWssPort() + "/sub";
    }

    private static void getDanmuHosts() {
        Request danmuInfoReq = new Request.Builder()
                .url("https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=" + roomId)
                .get().build();
        synchronized (monitor) {
            while (true) {
                try (Response danmuInfoResp = client.newCall(danmuInfoReq).execute()) {
                    ResponseBody body = danmuInfoResp.body();
                    if (null != body) {
                        JsonElement jsonElement = JsonParser.parseString(body.string());
                        JsonObject root = jsonElement.getAsJsonObject();
                        int code = root.get("code").getAsInt();
                        if (code != 0) {
                            System.out.println(root.get("msg").getAsString());
                            break;
                        }
                        JsonObject data = root.get("data").getAsJsonObject();
                        key = data.get("token").getAsString();
                        JsonArray hostList = data.get("host_list").getAsJsonArray();
                        danmuHosts = gson.fromJson(hostList, DanmuHost[].class);
                        break;
                    }
                } catch (IOException e) {
                    System.out.println("无法访问，请检查网络(3秒钟后自动重新获取)");
                    try {
                        monitor.wait(3000L);
                    } catch (InterruptedException ex) {
                        System.out.println("程序异常中断");
                    }
                }
            }
        }
    }

    private static void getRoomId(int id) {
        Request roomIdReq = new Request.Builder()
                .url("https://api.live.bilibili.com/room/v1/Room/room_init?id=" + id)
                .get().build();
        synchronized (monitor) {
            while (true) {
                try (Response roomIdResp = client.newCall(roomIdReq).execute()) {
                    ResponseBody body = roomIdResp.body();
                    if (null != body) {
                        JsonElement jsonElement = JsonParser.parseString(body.string());
                        JsonObject root = jsonElement.getAsJsonObject();
                        int code = root.get("code").getAsInt();
                        if (code != 0) {
                            System.out.println(root.get("msg").getAsString());
                            break;
                        }
                        JsonObject data = root.get("data").getAsJsonObject();
                        roomId = data.get("room_id").getAsInt();
                        break;
                    }
                } catch (IOException e) {
                    System.out.println("无法访问，请检查网络(3秒后自动重新解析)");
                    try {
                        monitor.wait(3000L);
                    } catch (InterruptedException ex) {
                        System.out.println("程序异常中断");
                    }
                }
            }
        }
    }

    static class DanmuWebSocketAdapter extends MyWebSocketAdapter {
        public DanmuWebSocketAdapter() {
            Timer timer = new Timer("ping-timer", true);
            TimerTask task = new TimerTask() {
                @Override
                public void run() {
                    if (!enterRoomSucceed) {
                        return;
                    }
                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    buffer.order(ByteOrder.BIG_ENDIAN)      // 大端模式/大字节序
                            .putInt(16)                     // 数据包总大小 = 头部长度 + 内容长度
                            .putShort((short) 16)           // 头部长度 （固定为16）
                            .putShort((short) 1)            // 协议版本 0 1 2 3
                            .putInt(2)                      // 操作类型 2 3 5 7 8 (2-心跳)
                            .putInt(1)                      // 序列id （固定为1）
                            .rewind();                      // buffer的position重置为0
                    danmuWebsocketClient.send(buffer);      // 空包，只包含头部信息
                }
            };
            timer.scheduleAtFixedRate(task, 0L, 30000L);    // 每30秒发送一次 心跳 ping
        }

        private boolean enterRoomSucceed;   // 是否成功接入房间弹幕

        @Override
        public void onOpen(ServerHandshake serverHandshake) {
            System.out.println("连接弹幕服务器成功");
            System.out.println("正在进入房间...");
            EnterRoomInfo enterRoomInfo = new EnterRoomInfo();
            enterRoomInfo.setProtover(3);
            enterRoomInfo.setRoomid(roomId);
            enterRoomInfo.setKey(key);
            String enterRoomJson = gson.toJson(enterRoomInfo);
            byte[] enterRoomJsonBytes = enterRoomJson.getBytes(StandardCharsets.UTF_8);
            int totalSize = 16 + enterRoomJsonBytes.length;
            ByteBuffer buffer = ByteBuffer.allocate(totalSize);
            buffer.order(ByteOrder.BIG_ENDIAN)      // 大端模式/大字节序
                    .putInt(totalSize)              // 数据包总大小 = 头部长度 + 内容长度
                    .putShort((short) 16)           // 头部长度 （固定为16）
                    .putShort((short) 1)            // 协议版本 0 1 2 3
                    .putInt(7)                      // 操作类型 2 3 4 5 7 8 (7-接入房间弹幕)
                    .putInt(1)                      // 序列id （固定为1）
                    .put(enterRoomJsonBytes)        // 内容
                    .rewind();                      // buffer的position重置为0
            danmuWebsocketClient.send(buffer);
        }

        @Override
        public void onMessage(ByteBuffer buffer) {
            int op = buffer.getInt(8);   //操作类型
            switch (op) {
                case 8: // 8-接入房间弹幕回应
                    System.out.println("进入房间成功，开始显示弹幕...");
                    enterRoomSucceed = true;
                    break;
                case 3: // 3-心跳回应
                    break;
                case 5: // 5-弹幕、广播等全部信息
                    short protoVer = buffer.getShort(6); //协议版本
                    byte[] src = getContent(buffer, 0);
                    byte[] decBytes = decompress(src, protoVer);
                    List<String> bodyList = new ArrayList<>();
                    if (protoVer == 0) {
                        bodyList.add(new String(decBytes, StandardCharsets.UTF_8));
                    } else {
                        ByteBuffer byteBuffer = ByteBuffer.wrap(decBytes);
                        while (byteBuffer.hasRemaining()) {
                            byte[] content = getContent(byteBuffer, byteBuffer.position());
                            bodyList.add(new String(content, StandardCharsets.UTF_8));
                        }
                    }
                    for (String body : bodyList) {
                        JsonElement jsonElement = JsonParser.parseString(body);
                        JsonObject root = jsonElement.getAsJsonObject();
                        String cmd = root.get("cmd").getAsString();
                        if ("DANMU_MSG".equals(cmd)) {
                            JsonArray info = root.get("info").getAsJsonArray();
                            String msg = info.get(1).getAsString();
                            JsonArray array = info.get(2).getAsJsonArray();
                            String name = array.get(1).getAsString();
                            System.out.println(name + ": " + msg);
                        }
                    }
                    break;
                default:
                    break;
            }

        }

        @Override
        public void onClose(int code, String reason, boolean remote) {
            if (code == -1) {
                System.out.println("无法连接，请检查网络");
            } else {
                System.out.println("房间连接断开");
            }
            enterRoomSucceed = false;
            hostIndex++;
            if (hostIndex >= danmuHosts.length) {
                hostIndex = 0;
                danmuHosts = null;
                System.out.println("重新获取弹幕服务器...");
                getDanmuHosts();
                if (danmuHosts == null || danmuHosts.length == 0) {
                    return;
                }
                System.out.println("获取弹幕服务器成功");
            }
            String url = getWsUrl();
            System.out.println("尝试再次连接弹幕服务器...");
            danmuWebsocketClient = new DanmuWebSocketClient(URI.create(url));
            danmuWebsocketClient.setListener(this);
            danmuWebsocketClient.connect();
        }

        /**
         * 获取消息正文内容
         */
        private byte[] getContent(ByteBuffer bytes, int offset) {
            int totalSize = bytes.getInt(offset);    // 包总大小
            byte[] dst = new byte[totalSize - 16];    // 内容大小
            bytes.position(offset + 16);
            bytes.get(dst, 0, dst.length);
            return dst;
        }

        /**
         * 解压缩消息正文
         */
        private byte[] decompress(byte[] src, short protoVer) {
            InputStream is = null;
            if (3 == protoVer) {        // 3 - brotli压缩算法
                try {
                    is = new BrotliCompressorInputStream(new ByteArrayInputStream(src));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (2 == protoVer) { // 2 - deflate压缩算法
                is = new DeflateCompressorInputStream(new ByteArrayInputStream(src));
            } else {                    // 0 - 未使用压缩算法
                is = new ByteArrayInputStream(src);
            }
            if (is == null) {
                return new byte[0];
            }
            try (InputStream inputStream = is;
                 ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                byte[] buf = new byte[1024];
                int len;
                while ((len = inputStream.read(buf)) != -1) {
                    outputStream.write(buf, 0, len);
                }
                return outputStream.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return new byte[0];
        }

    }

}
